react16特性:fiber reconciler解密

五月 06, 2019

前篇

React是一款用于构建用户界面的库,React会跟踪组件内的状态变化并将这些变化反应到页面上,当我们使用setState方法,React会检测组件的状态及属性是否改变,从而重新渲染组件到界面上,这种机制被称为reconciliation(协调)

对此机制,React对其中关键要素给出了高度的概括,涉及到:React元素生命周期函数render方法、以及判断组件变化的Diff算法。经过React组件render方法返回的元素树具有不可变性,通常被称为Virtual DOM,这东西的叫法很早就出现了,用于帮助一些人理解,但目前看来也给许多人带来了困惑,而在这篇文章里,我将坚持称它为React elements tree

除了React elements tree,React框架里还维护着一棵内部实例(组件,Dom节点等)树用于保持应用的状态。从React16开始,React推出一种新的管理内部实例树的算法,而它就是Fiber

在本篇文章里,我会对这些重要的概念、涉及到的算法数据结构做一个深度的介绍。

准备例子

这里有一个简单的应用示例,Counter如下:

counter.gif

class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
        this.handleClick = this.handleClick.bind(this);
    }

    handleClick() {
        this.setState((state) => {
            return { count: state.count + 1 };
        });
    }
    render() {
        return (
            <div style={{ margin: "200px auto", textAlign: "center" }}>
                <span>{this.state.count}</span>
                <button onClick={this.handleClick}>click</button>
            </div>
        );
    }
}

如上代码所示,此Counter由一个buttonspan组成,点击button按钮,span的内容会改变+1。

在整个reconciliation期间,React会存在大量的操作,比如在本例子中:

还有一些其他的操作,比如调用生命周期方法(LifeCycle methods)或者更新元素的引用(Refs),所有这一些操作,在Fiber架构中被称为Work(工作单元)。不同种类的React元素对应不同的Work,比如Class组件,React需要创建一个组件的实例,而不会在Functional组件上执行。React里有很多种React元素类型,比如class和fuctional组件、host组件(如dom节点)、portals等等。React元素的类型(type)由函数React.crateElement的第一参数决定。

在正式介绍这些操作和Fiber算法内容之前,我们必须要熟悉下React内部定义的数据结构。

React Elements与Fiber Node

ReactDom在调用render方法(ReactDom.render(<XXX />, container))的时候,会通过由new ReactRoot创建Container,然后并调用root.render();

// 这里代码是从React源码中截取
function ReactRoot(
  container: DOMContainer,
  isConcurrent: boolean,
  hydrate: boolean,
) {
  const root = createContainer(container, isConcurrent, hydrate);
  this._internalRoot = root;
}

ReactRoot.prototype.render = function(
  children: ReactNodeList,
  callback: ?() => mixed,
): Work {
  const root = this._internalRoot;
  const work = new ReactWork();
  ...
  updateContainer(children, root, null, work._onCommit);
  ...
};

所以我们看看这些参数的数据结构:

在reconciliation期间,React组件的render所产生的ReactElement会被合并到Fiber树中,并且每一个ReactElement都有一个对应的Fiber节点。对于不同类型的元素,React需要做不同的操作。在我们的例子中,对于class组件Counter,React会调用生命周期方法、render方法;而对于span元素,其被称为host组件(DOM节点),React会执行必要的DOM变更。因此,每一个React元素,根据其类型转为对应类型的Fiber节点,而Fiber节点的类型告诉React要对此类节点做些什么。

当一个React元素首次被转换为Fiber节点的时候,React会使用Element对象作为参数,来调用createFiberFromTypeAndProps。随后的更新,React会复用这些Fiber节点,根据新生成的element数据,仅仅更新那些需要改变的属性。React可能会根据元素的key属性,在Fiber结构中移动或移除节点。

因为React为每个React元素创建一个Fiber节点,并且可以从Fiber的数据结构中return child sibling字段看出,这些元素将组成的一个Fiber节点树(其实应该说是链式节点更准确)。对于我们的示例应用程序,它看起来像这样:
02.jpg

为什么会使用链式存储Fiber节点,请看另一篇文章

首次渲染之后,React便拥有了一颗Fiber节点树,它是用以呈现当前页面UI的状态。而这颗树在Fiber里也被引用为current,

const uninitializedFiber = createHostRootFiber(isConcurrent);
root.current = uninitializedFiber;

而当应用状态更新的时候,React会构建一个workInProgresstree(其实也是一个链表),它用来更新一个未来的UI状态,所有的操作都发生在workInProgress树上。在React遍历current树时候,会对fiber节点创建一个克隆的alternate节点,这些alternate节点就会组建成workInProgress树。应用的状态更新发生在alternate节点上,当全部的alternate节点的更新(work)得到处理完成时(还未重新绘制到界面上,只是节点属性的更新了),这个时候workInProgress树就生成完毕。最后workInProgress树被绘制到界面上后,这个workInProgress树就会变成current树。

React根据遍历WrokInProgress树,在每一个节点中会根据WrokInProgress树current的tag,也就是Fiber的标识符,用来区分比如updateFunctionalComponentupdateClassComponent等操作,这些函数调用里会构造下一节点child,比如updateFunctionComponent -> nextChildren = renderWithHooks(), updateClassComponent -> nextChildren = intance.render();

switch (workInProgress.tag) {
    case IndeterminateComponent: {
      ...
    }
    case LazyComponent: {
      ...
    }
    case FunctionComponent: {
      ...
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderExpirationTime,
      );
    }
    case ClassComponent: {
      ...
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderExpirationTime,
      );
    }
    case HostRoot:
      ...
    case HostComponent:
      ...
    case HostText:
     ...
    case SuspenseComponent:
      ...
    case HostPortal:
      ...
    case ForwardRef: {
      ...
    }
    case Fragment:
      ...
    case Mode:
      ...
    case Profiler:
      ...
    case ContextProvider:
      ...
    case ContextConsumer:
      ...
    case MemoComponent: {
      ...
    }
    case SimpleMemoComponent: {
      ...
    }
    case IncompleteClassComponent: {
      ...
    }
    case DehydratedSuspenseComponent: {
      ...
    }
    case EventComponent: {
      ...
    }
    case EventTarget: {
      ...
    }
}

function updateFunctionComponent() {
    ...
    nextChildren = renderWithHooks(
        current,
        workInProgress,
        Component,
        nextProps,
        context,
        renderExpirationTime,
    );
}

function updateClassComponent() {
    ...
    const nextUnitOfWork = finishClassComponent(
        current,
        workInProgress,
        Component,
        shouldUpdate,
        hasContext,
        renderExpirationTime,
    );
}

function finishCalssComponent() {
    ...
    nextChildren = instance.render();
}

在这些Update操作过程中,每一个Fiber节点都会查看是否有子节点,并计算出来,就得到了nextChildren,也就是ReactELement,再通过reconcileChildren并把它封装包裹到Fiber节点中,然后组成Fiber和workInProgress节点树。

function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderExpirationTime: ExpirationTime,
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderExpirationTime,
    );
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderExpirationTime,
    );
  }
}

根据之前说到的,workInProgress树遍历完后,这个时候只是FiberNode的alternateNode,并没有呈现到ui上,为此在没有呈现到ui上的阶段,在React里被称为Render Phase,这一阶段是可以被中断, 而workInProgress树遍历完后,即Render Phase结束后,会有一个标识符workInProgressRootExitStatus = RootCompleted,也就是completeWork()函数执行完后,会将此标志workInProgressRootExitStatus赋值为RootCompleted,此时workInProgress current也为null了,则会退出workLoop, 然后根据workInProgressRootExitStatus判断Loop结束状态为RootCompleted,则React进入到Commit Phase,这一阶段是不能被中断的。涉及到以下函数调用过程:

function renderRoot(
  root: FiberRoot,
  expirationTime: ExpirationTime,
  isSync: boolean,
): SchedulerCallback | null {
    do {
      try {
        if (isSync) {
          workLoopSync();
        } else {
          workLoop();
        }
        break;
      } catch (thrownValue) {
        ...
      }
    } while (true);

    switch (workInProgressRootExitStatus) {
        ... 
        case RootCompleted: {
        // The work completed. Ready to commit.
            return commitRoot.bind(null, root, expirationTime);
        }
    }
}

function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

function workLoop() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
  ...
  next = beginWork(current, unitOfWork, renderExpirationTime);
  
  if (next === null) {
    next = completeUnitOfWork(unitOfWork);
  }

  ...

  return next;
}

beginWork = (current, unitOfWork, expirationTime) => {
    ... 
    return originalBeginWork(current, unitOfWork, expirationTime);
};

function originalBeginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
): Fiber | null {
    ...
    switch (workInProgress.tag) {
        ...
        return updateClassComponent()
    }
}

function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
    ...
    next = completeWork(current, workInProgress, renderExpirationTime);

    if (workInProgressRootExitStatus === RootIncomplete) {
        workInProgressRootExitStatus = RootCompleted;
    }
}

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
): Fiber | null {
  ...
  switch (workInProgress.tag) {
    ...
  }
}

可参考以下流程图:

reconcilier


![01.jpg]
https://mermaidjs.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggVEQ7XG5yb290LS0-RmliZXJSb290O1xuRmliZXJSb290W2N1cnJlbnRdLS0-SG9zdEZpYmVySG9zdDtcbkhvc3RGaWJlckhvc3Rbc3RhdGVOb2RlXS0tPnJvb3Q7XG5GaWJlclJvb3RbY29udGFpbmVySW5mb10tLT5jb250YWluZXJEb21cbmNvbnRhaW5lckRvbS0tPkZpYmVyUm9vdFtjb250YWluZXJJbmZvXSIsIm1lcm1haWQiOnsidGhlbWUiOiJkZWZhdWx0In19
graph TD;
FiberRoot–>FiberRootNode;
FiberRootNode–>FiberRoot;
FiberRootNode–>FiberRootNode.containerInfo
FiberRootNode.containerInfo–>containerDom
containerDom–>FiberRootNode.containerInfo
FiberRootNode–FiberRootNode.current–>HostFiberHost;
HostFiberHost–HostFiberHost.stateNode–>FiberRoot
![02.jpg]
graph LR;
HostRoot–child–>Counter
Counter–return–>HostRoot
Counter–child–>div
div–return–>Counter
div–child–>span
span–return–>div
span–sibling–>button
button–return–>div